Skip to content

feat: export caption sidecars and merge editor follow-ups#686

Merged
webadderall merged 11 commits into
mainfrom
feat/export-caption-sidecars
Jun 22, 2026
Merged

feat: export caption sidecars and merge editor follow-ups#686
webadderall merged 11 commits into
mainfrom
feat/export-caption-sidecars

Conversation

@webadderall

@webadderall webadderall commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Validation

  • ./node_modules/.bin/vitest --run src/components/video-editor/captionEditing.test.ts
  • ./node_modules/.bin/tsc --noEmit --noUnusedLocals false --noUnusedParameters false

Summary by CodeRabbit

Release Notes

  • New Features

    • Separate webcam width and height controls for independent sizing
    • Inline caption editing directly in video playback
    • Project save dialog with rename and copy options
    • Extended vertical padding range (up to 250)
    • Unsaved changes warnings when opening projects
  • Improvements

    • Enhanced webcam crop preview alignment
    • Improved export bitrate calculations
  • Localization

    • Added webcam dimension labels in 10 languages

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bb771f45-7ab1-4c58-8464-dd29170a3180

📥 Commits

Reviewing files that changed from the base of the PR and between 9f6cb6e and def1bac.

📒 Files selected for processing (5)
  • electron/electron-env.d.ts
  • electron/preload.ts
  • src/components/video-editor/SettingsPanel.tsx
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/modernFrameRenderer.ts

📝 Walkthrough

Walkthrough

The PR adds rename/copy save modes to the project save IPC handler and editor modal flows, converts webcam overlay sizing from a scalar to independent width/height with crop-matched height computation, extends the advanced vertical padding range to 250, adds inline caption cue editing in VideoPlayback, and updates export pipeline components including annotation transforms, cursor scaling, bitrate multipliers, and native static-layout export logic.

Changes

Project Save Mode and Unsaved-Changes Dialog Flow

Layer / File(s) Summary
IPC save trust checks and named save mode
electron/ipc/register/project.ts, electron/electron-env.d.ts, electron/preload.ts
Adds NamedProjectSaveMode normalization and tighter existingProjectPath trust checks in save-project-file; in save-project-file-named, branches on mode to either generate a fresh projectId (copy) or preserve identity and clean up the old file (rename). Threads the new mode parameter through the preload wrapper and TypeScript API surface.
Editor dialog state and promise helpers
src/components/video-editor/VideoEditor.tsx
Introduces PendingProjectSaveDialog and unsaved-changes decision types, new React state and refs for both dialogs, an input-focus effect, and promise-based openProjectSaveDialog/openUnsavedChangesDialog control helpers.
Save submit paths with copy/rename modes
src/components/video-editor/VideoEditor.tsx
Updates saveProject to open the Save Project dialog when no target path exists, extends saveProjectWithName with a mode argument forwarded to IPC, adds handleProjectSaveDialogSubmit using "copy" mode, and routes inline rename through "rename" mode.
Unsaved-changes gating and dialog rendering
src/components/video-editor/VideoEditor.tsx
Adds confirmReplaceSourceWithUnsavedChanges gating for project open and media import flows, and renders save/unsaved dialogs across loading, error, and main layout branches.

Webcam Dimensions, Crop Handling, and Advanced Padding

Layer / File(s) Summary
Shared webcam/padding contracts, normalization, and layout math
src/components/video-editor/types.ts, src/components/video-editor/projectPersistence.ts, src/components/video-editor/projectPersistence.test.ts, src/components/video-editor/videoPlayback/layoutUtils.ts, src/components/video-editor/videoPlayback/layoutUtils.test.ts
Adds width/height to WebcamOverlaySettings and ADVANCED_VERTICAL_PADDING_MAX = 250; updates persistence padding clamps and webcam normalization; extends padded layout math to allow top/bottom padding up to 250 with directional offset centering, with new tests.
Webcam overlay dimensions/position/crop helpers with tests
src/components/video-editor/webcamOverlay.ts, src/components/video-editor/webcamOverlay.test.ts
Adds getWebcamOverlayDimensionsPx, updates getWebcamOverlayPosition to resolve independent width/height, and adds getCropMatchedWebcamHeightPercent with tests covering linked/unlinked height and default crop fallback.
Settings controls and locale updates for width/height
src/components/video-editor/SettingsPanel.tsx, src/i18n/locales/*/settings.json
Splits the single "Webcam Size" slider into "Webcam Width" and "Webcam Height" controls, updates advanced padding slider bounds to ADVANCED_VERTICAL_PADDING_MAX, and adds webcamWidth/webcamHeight keys in all 10 locale files.
Webcam crop control callback and resize refactor
src/components/video-editor/WebcamCropControl.tsx
Changes onCropChange signature to include optional previewFrame, replaces aspect-ratio-dependent crop math with normalizeWebcamCropRegion-based helpers, and propagates previewFrame through all commit paths.
Playback webcam width/height layout application
src/components/video-editor/VideoPlayback.tsx, src/components/video-editor/VideoEditor.tsx
Derives webcamWidth/webcamHeight from independent settings with crop-matched height, drives bubble layout through getWebcamOverlayDimensionsPx, and styles squircle/shadow from explicit pixel dimensions.

Inline Caption Cue Editing

Layer / File(s) Summary
Caption word fallback normalization
src/components/video-editor/captionEditing.ts
Filters invalid/empty word entries before building sourceWords, falling back to cue text when no valid words remain.
Playback caption editing session UI
src/components/video-editor/VideoPlayback.tsx
Adds onEditAutoCaption prop, session state with captionEditSessionRef, beginCaptionEdit/commitCaptionEdit/cancelCaptionEdit handlers, textarea overlay rendering, and accessible caption box interaction.
VideoEditor caption edit wiring and sidecar toggle
src/components/video-editor/VideoEditor.tsx
Imports caption utilities, wires handleSaveAutoCaptionEdit callback via updateCaptionCuesForEditedTarget, passes onEditAutoCaption to VideoPlayback, and changes includeCaptionSidecar default to false.

Render and Export Pipeline Behavior Updates

Layer / File(s) Summary
Playback annotation transforms and keyframe navigation
src/components/video-editor/VideoPlayback.tsx, src/components/video-editor/VideoEditor.tsx
Adds annotationSceneTransform/annotationRecordingRect state, tracks scene transform in zoom ticker, removes transition interpolation, wraps annotation overlays in a transform container, and updates editor keyframe skip search logic.
Cursor min viewport scaling and texture behavior
src/components/video-editor/videoPlayback/cursorRenderer.ts, src/components/video-editor/VideoPlayback.tsx
Extends CursorRenderConfig with minViewportScale, threads it through getCursorViewportScale and Pixi/canvas draw paths, enables mipmap auto-generation on cursor textures, and initializes the playback cursor with minViewportScale: 0.
Export bitrate multiplier and expectation updates
src/lib/exporter/exportBitrate.ts, src/lib/exporter/exportBitrate.test.ts
Raises "quality" multiplier to 1 and "balanced" multiplier to 0.75, with updated test expectations across source and native-static-layout scenarios.
FrameRenderer annotation and webcam draw path updates
src/lib/exporter/frameRenderer.ts
Removes computeFocusFromTransform and the transition interpolation branch from updateAnimationState; refactors drawWebcamOverlay to use getCropMatchedWebcamHeightPercent/getWebcamOverlayDimensionsPx/getWebcamOverlayPosition for sizing and shadow rendering.
ModernFrameRenderer sceneTransform, layout, and zoom updates
src/lib/exporter/modernFrameRenderer.ts, src/lib/exporter/modernFrameRenderer.test.ts
Adds optional sceneTransform to composeBlurAnnotationFrame, replaces WebcamLayoutCache.size with width/height, updates webcam layout matching/apply/update logic, passes sampled sceneTransform in temporal blur, uses scalePreviewBorderRadius helper, and removes transition-progress interpolation with test coverage.
Native static-layout exporter checks and flags
src/lib/exporter/modernVideoExporter.ts, src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts
Adds hasUnsupportedNativeStaticLayoutWebcamShape and the "unsupported-rectangular-webcam-overlay" skip reason, prefers webcam.width for sizePercent, removes transition-based zoom telemetry interpolation, and forwards experimentalNvidiaCudaExport.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant VideoEditor
  participant SaveDialog as Save Project Dialog
  participant IPC as Electron IPC
  rect rgba(100, 149, 237, 0.5)
    Note over User, IPC: Save As / New Project Name
    User->>VideoEditor: saveProject({ saveAs: true })
    VideoEditor->>SaveDialog: openProjectSaveDialog()
    SaveDialog-->>User: show dialog, focus input
    User->>SaveDialog: submit name
    SaveDialog->>VideoEditor: handleProjectSaveDialogSubmit(name)
    VideoEditor->>IPC: saveProjectFileNamed(data, name, thumb, "copy")
    IPC->>IPC: randomUUID → withProjectId(data, newId)
    IPC-->>VideoEditor: { path, projectId }
    VideoEditor-->>SaveDialog: resolveProjectSaveDialog(success)
  end
  rect rgba(144, 238, 144, 0.5)
    Note over User, IPC: Inline Rename
    User->>VideoEditor: submit inline rename
    VideoEditor->>IPC: saveProjectFileNamed(data, name, thumb, "rename")
    IPC->>IPC: ensureProjectDataHasProjectId, unlink old file
    IPC-->>VideoEditor: { path, projectId }
  end
Loading
sequenceDiagram
  participant User
  participant VideoPlayback
  participant CaptionBox
  participant VideoEditor
  User->>CaptionBox: click caption text
  CaptionBox->>VideoPlayback: beginCaptionEdit(target)
  VideoPlayback->>VideoPlayback: setCaptionEditSession(draft, target)
  VideoPlayback->>VideoPlayback: focus textarea overlay
  User->>VideoPlayback: type edit, press Enter
  VideoPlayback->>VideoPlayback: commitCaptionEdit()
  VideoPlayback->>VideoEditor: onEditAutoCaption(target, normalizedText)
  VideoEditor->>VideoEditor: updateCaptionCuesForEditedTarget(...)
  VideoEditor-->>User: success toast
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • webadderallorg/Recordly#304: Directly overlaps in electron/ipc/register/project.ts and electronAPI.saveProjectFileNamed typings — both implement the named project save persistence flow including identity handling and old-project cleanup.
  • webadderallorg/Recordly#394: Both modify WebcamCropControl.tsx and VideoPlayback.tsx with related webcam overlay helpers and crop behavior in the editor and during rendering.
  • webadderallorg/Recordly#630: Both modify cursorRenderer.ts, extending CursorRenderConfig and adjusting cursor texture/mipmap behavior.

Suggested labels

Checked

🐇 A rabbit hops through code with glee,
Width and height now roam free!
Rename or copy — pick your mode,
Captions editable down the road.
Bitrates boosted, zoom paths clear,
The prettiest export of the year! 🎬

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides a clear summary section, validation instructions, and references the upstream PRs merged, but lacks structured detail on motivation, change type, testing guide, and completeness per the template. Expand the description to include explicit Type of Change selection, comprehensive Testing Guide with steps/environment setup, and Related Issue(s) references for better clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main deliverables: caption sidecar export feature plus editor follow-ups from upstream merges.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/export-caption-sidecars

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/video-editor/VideoEditor.tsx (1)

628-653: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate sidecar export on cue data, not burn-in caption visibility.

Line 628 requires autoCaptionSettings.enabled, but generating captions only updates autoCaptions at Lines 2852-2853. In the default generated-caption flow, valid cues can exist while the sidecar option stays hidden and no payload is sent. Use the validated sidecar cue list as the eligibility check instead.

🐛 Proposed fix
-	const hasCaptionsForSidecar = autoCaptionSettings.enabled && autoCaptions.length > 0;
 	const captionSidecarCues = useMemo(
 		() =>
 			autoCaptions
@@
 				})),
 		[autoCaptions],
 	);
+	const hasCaptionsForSidecar = captionSidecarCues.length > 0;
 	const captionSidecarPayload =
-		hasCaptionsForSidecar && captionSidecarCues.length > 0 && includeCaptionSidecar
+		hasCaptionsForSidecar && includeCaptionSidecar
 			? {
 					format: "both" as const,
 					cues: captionSidecarCues,

Also applies to: 2852-2853, 6081-6083

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 628 - 653, The
hasCaptionsForSidecar variable on line 628 gates the sidecar export based on
autoCaptionSettings.enabled, but this doesn't guarantee valid captions exist.
Instead of checking autoCaptionSettings.enabled and autoCaptions.length, use the
validated captionSidecarCues array as the eligibility check. Replace the
hasCaptionsForSidecar definition to check if captionSidecarCues has items, so
the sidecar payload is only sent when actual valid cue data is present, not just
when the caption setting is enabled.
🧹 Nitpick comments (2)
src/components/video-editor/projectPersistence.test.ts (1)

6-46: ⚡ Quick win

Add coverage for webcam width/height normalization branches.

This suite only asserts padding behavior, but normalizeProjectEditor in this PR also changed webcam.width/webcam.height fallback/clamp logic. Add focused assertions for explicit width/height, legacy size fallback, and clamping.

Proposed test additions
 describe("normalizeProjectEditor", () => {
+	it("normalizes webcam width/height from explicit values and legacy size fallback", () => {
+		const explicit = normalizeProjectEditor({
+			webcam: { width: 88, height: 36, size: 50 } as never,
+		});
+		expect(explicit.webcam.width).toBe(88);
+		expect(explicit.webcam.height).toBe(36);
+
+		const legacy = normalizeProjectEditor({
+			webcam: { size: 55 } as never,
+		});
+		expect(legacy.webcam.width).toBe(55);
+		expect(legacy.webcam.height).toBe(55);
+	});
+
+	it("clamps webcam width/height to allowed range", () => {
+		const editor = normalizeProjectEditor({
+			webcam: { width: 2, height: 1000 } as never,
+		});
+		expect(editor.webcam.width).toBe(10);
+		expect(editor.webcam.height).toBe(100);
+	});
+
 	it("preserves the extended advanced vertical padding range", () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/projectPersistence.test.ts` around lines 6 - 46,
The test suite for normalizeProjectEditor currently only covers padding
normalization behavior and is missing test coverage for the webcam width/height
normalization logic that was also changed. Add new test cases to the describe
block that verify the webcam width/height handling in normalizeProjectEditor,
including tests for: explicit width/height values being preserved, legacy size
property fallback when width/height are not provided, and proper clamping of
webcam dimensions to their maximum values. These tests should follow the same
pattern as the existing padding tests using expect and toMatchObject assertions.
electron/ipc/register/project.ts (1)

301-305: 💤 Low value

Consider resolving symlinks for defense-in-depth.

The isPathInsideDirectory check uses path normalization but doesn't resolve symlinks. A symlink inside projectsDir pointing outside would pass this check, and fs.writeFile would follow the symlink to write outside the trusted directory.

In a desktop app where users have full filesystem control, this is low-risk (users would be "attacking themselves"). However, for defense-in-depth, consider using fs.realpath before the directory containment check:

🛡️ Optional hardening
+      const resolvedExistingPath = existingProjectPath
+        ? await fs.realpath(existingProjectPath).catch(() => path.resolve(existingProjectPath))
+        : null;
       const trustedExistingProjectPath = existingProjectPath &&
         path.extname(existingProjectPath).toLowerCase() === `.${PROJECT_FILE_EXTENSION}` &&
-        (isTrustedProjectPath(existingProjectPath) || isPathInsideDirectory(existingProjectPath, projectsDir))
-        ? path.resolve(existingProjectPath)
+        (isTrustedProjectPath(existingProjectPath) || isPathInsideDirectory(resolvedExistingPath!, projectsDir))
+        ? resolvedExistingPath
         : null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/register/project.ts` around lines 301 - 305, The
trustedExistingProjectPath assignment does not resolve symlinks before
validating containment with isPathInsideDirectory, which could allow a symlink
inside projectsDir to escape the trusted boundary. To fix this, resolve the
existingProjectPath using fs.realpathSync before passing it to the
isPathInsideDirectory check, ensuring the actual target location of any symlinks
is validated as being within projectsDir rather than the symlink itself.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 3925-3927: In the getCropMatchedWebcamHeightPercent function call,
the second argument is incorrectly passing webcamWidth when it should pass
webcamHeight. This causes the height to be recalculated based on width every
time, overwriting user manual height adjustments. Replace the second webcamWidth
argument with webcamHeight to preserve the user's previously set height value
when applying crop updates.

In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 2390-2404: The AnnotationOverlay component is receiving the
interactionScale prop but not using it, which prevents the Rnd component from
accounting for zoom scale during drag and resize operations. Add
interactionScale as a property to the AnnotationOverlayProps interface, then
destructure interactionScale in the AnnotationOverlay component signature, and
finally pass this interactionScale value to the Rnd component's scale prop to
ensure drag and resize deltas are calculated correctly under zoom conditions.

In `@src/lib/exporter/modernFrameRenderer.ts`:
- Around line 2569-2571: The getCropMatchedWebcamHeightPercent method is
receiving already-cropped dimensions from renderableWebcamSource after
refreshWebcamFrameCache has created a crop-trimmed cache, which double-applies
crop aspect compensation and skews webcam overlay proportions. Instead of
passing renderableWebcamSource.width and renderableWebcamSource.height to
getCropMatchedWebcamHeightPercent, pass the original uncropped source dimensions
from liveSource (liveSourceWidth and liveSourceHeight) to ensure crop-matched
height is calculated correctly without double compensation.

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 628-653: The hasCaptionsForSidecar variable on line 628 gates the
sidecar export based on autoCaptionSettings.enabled, but this doesn't guarantee
valid captions exist. Instead of checking autoCaptionSettings.enabled and
autoCaptions.length, use the validated captionSidecarCues array as the
eligibility check. Replace the hasCaptionsForSidecar definition to check if
captionSidecarCues has items, so the sidecar payload is only sent when actual
valid cue data is present, not just when the caption setting is enabled.

---

Nitpick comments:
In `@electron/ipc/register/project.ts`:
- Around line 301-305: The trustedExistingProjectPath assignment does not
resolve symlinks before validating containment with isPathInsideDirectory, which
could allow a symlink inside projectsDir to escape the trusted boundary. To fix
this, resolve the existingProjectPath using fs.realpathSync before passing it to
the isPathInsideDirectory check, ensuring the actual target location of any
symlinks is validated as being within projectsDir rather than the symlink
itself.

In `@src/components/video-editor/projectPersistence.test.ts`:
- Around line 6-46: The test suite for normalizeProjectEditor currently only
covers padding normalization behavior and is missing test coverage for the
webcam width/height normalization logic that was also changed. Add new test
cases to the describe block that verify the webcam width/height handling in
normalizeProjectEditor, including tests for: explicit width/height values being
preserved, legacy size property fallback when width/height are not provided, and
proper clamping of webcam dimensions to their maximum values. These tests should
follow the same pattern as the existing padding tests using expect and
toMatchObject assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bbab6d85-a7c5-48cb-94dd-db98892ba0ba

📥 Commits

Reviewing files that changed from the base of the PR and between 952d63d and 9f6cb6e.

📒 Files selected for processing (34)
  • electron/ipc/register/export.ts
  • electron/ipc/register/project.ts
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/WebcamCropControl.tsx
  • src/components/video-editor/captionEditing.test.ts
  • src/components/video-editor/captionEditing.ts
  • src/components/video-editor/captionLayout.ts
  • src/components/video-editor/projectPersistence.test.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorRenderer.ts
  • src/components/video-editor/videoPlayback/layoutUtils.test.ts
  • src/components/video-editor/videoPlayback/layoutUtils.ts
  • src/components/video-editor/webcamOverlay.test.ts
  • src/components/video-editor/webcamOverlay.ts
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/it/settings.json
  • src/i18n/locales/ko/settings.json
  • src/i18n/locales/nl/settings.json
  • src/i18n/locales/pt-BR/settings.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-TW/settings.json
  • src/lib/exporter/exportBitrate.test.ts
  • src/lib/exporter/exportBitrate.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/modernFrameRenderer.test.ts
  • src/lib/exporter/modernFrameRenderer.ts
  • src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts
  • src/lib/exporter/modernVideoExporter.ts

Comment thread src/components/video-editor/SettingsPanel.tsx Outdated
Comment on lines +2390 to +2404
setAnnotationSceneTransform((current) => {
if (
Math.abs(current.scale - appliedTransform.scale) < 0.001 &&
Math.abs(current.x - appliedTransform.x) < 0.1 &&
Math.abs(current.y - appliedTransform.y) < 0.1
) {
return current;
}

return {
scale: appliedTransform.scale,
x: appliedTransform.x,
y: appliedTransform.y,
};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify AnnotationOverlay consumes interactionScale and forwards it to the Rnd scale prop.
annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
test -n "$annotation_file" || { echo "AnnotationOverlay.tsx not found"; exit 1; }

# Expect: interactionScale is declared in props/destructuring and <Rnd ... scale={interactionScale}> is present.
rg -n -C 4 'interactionScale|sceneTransform|recordingRect|<Rnd|scale=' "$annotation_file"

Repository: webadderallorg/Recordly

Length of output: 238


🏁 Script executed:

annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
test -n "$annotation_file" || { echo "AnnotationOverlay.tsx not found"; exit 1; }

# Get file stats and show the component signature and full Rnd element
echo "=== File: $annotation_file ==="
wc -l "$annotation_file"
echo ""
echo "=== Component signature (first 100 lines) ==="
head -n 100 "$annotation_file"

Repository: webadderallorg/Recordly

Length of output: 3305


🏁 Script executed:

annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
# Search for component export and interface/type definitions
echo "=== Component export and props type ==="
rg -n 'export|interface|type.*Props|function AnnotationOverlay' "$annotation_file" | head -n 20

Repository: webadderallorg/Recordly

Length of output: 183


🏁 Script executed:

annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
# Get the full Rnd element to see all its props
echo "=== Full Rnd element with props ==="
rg -n '<Rnd' -A 50 "$annotation_file" | head -n 80

Repository: webadderallorg/Recordly

Length of output: 2191


🏁 Script executed:

video_file="$(fd -i '^VideoPlayback\.tsx$' . | head -n 1)"
# Search for where AnnotationOverlay is used/imported and see if interactionScale is passed
rg -n 'AnnotationOverlay|interactionScale' "$video_file" | head -n 50

Repository: webadderallorg/Recordly

Length of output: 228


🏁 Script executed:

video_file="$(fd -i '^VideoPlayback\.tsx$' . | head -n 1)"
# Get context around where AnnotationOverlay component is rendered
rg -n -B 5 -A 15 '<AnnotationOverlay' "$video_file"

Repository: webadderallorg/Recordly

Length of output: 806


🏁 Script executed:

video_file="$(fd -i '^VideoPlayback\.tsx$' . | head -n 1)"
# Get full AnnotationOverlay component with all props
rg -n -A 30 '<AnnotationOverlay' "$video_file" | head -n 40

Repository: webadderallorg/Recordly

Length of output: 1353


🏁 Script executed:

# Also check the lines around 2390-2404 in VideoPlayback to see annotationSceneTransform usage
video_file="$(fd -i '^VideoPlayback\.tsx$' . | head -n 1)"
sed -n '2385,2410p' "$video_file"

Repository: webadderallorg/Recordly

Length of output: 680


🏁 Script executed:

# Check full AnnotationOverlayProps interface definition
annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
sed -n '7,20p' "$annotation_file"

Repository: webadderallorg/Recordly

Length of output: 537


🏁 Script executed:

# Check if sceneTransform and recordingRect are in the props but I missed them
annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
rg -n 'sceneTransform|recordingRect|interactionScale' "$annotation_file"

Repository: webadderallorg/Recordly

Length of output: 49


🏁 Script executed:

video_file="$(fd -i '^VideoPlayback\.tsx$' . | head -n 1)"
# Search for where annotationSceneTransform is declared/initialized
rg -n 'annotationSceneTransform|setAnnotationSceneTransform' "$video_file" | head -n 20

Repository: webadderallorg/Recordly

Length of output: 427


🏁 Script executed:

# Check the <Rnd> props in Rnd documentation or react-rnd to understand what scale prop does
# Also check if there are other components using scale prop with Rnd
rg -n 'scale=' | grep -i rnd | head -n 10

Repository: webadderallorg/Recordly

Length of output: 49


🏁 Script executed:

# Check the exact signature/destructuring of AnnotationOverlay function to see if interactionScale is there
annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
sed -n '19,35p' "$annotation_file"

Repository: webadderallorg/Recordly

Length of output: 551


🏁 Script executed:

# Double check the complete props interface and what's actually being passed from VideoPlayback
annotation_file="$(fd -i '^AnnotationOverlay\.tsx$' . | head -n 1)"
cat "$annotation_file" | head -n 50

Repository: webadderallorg/Recordly

Length of output: 1667


🏁 Script executed:

# Verify lines 3322-3399 in VideoPlayback to understand "Also applies to"
video_file="$(fd -i '^VideoPlayback\.tsx$' . | head -n 1)"
sed -n '3320,3330p' "$video_file"

Repository: webadderallorg/Recordly

Length of output: 425


Add interactionScale prop to AnnotationOverlay and pass it to Rnd's scale prop.

The overlay is CSS-transformed with annotationSceneTransform, but interactionScale is being passed to AnnotationOverlay (line 3398 in VideoPlayback) without being consumed. The AnnotationOverlayProps interface and component destructuring don't include it, and the <Rnd> component doesn't receive it as a scale prop. Without this, drag/resize deltas will be calculated in screen pixels rather than accounting for the zoom scale, causing persisted coordinates to be incorrect under zoom.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoPlayback.tsx` around lines 2390 - 2404, The
AnnotationOverlay component is receiving the interactionScale prop but not using
it, which prevents the Rnd component from accounting for zoom scale during drag
and resize operations. Add interactionScale as a property to the
AnnotationOverlayProps interface, then destructure interactionScale in the
AnnotationOverlay component signature, and finally pass this interactionScale
value to the Rnd component's scale prop to ensure drag and resize deltas are
calculated correctly under zoom conditions.

Comment thread src/lib/exporter/modernFrameRenderer.ts
@webadderall webadderall merged commit d8e34a1 into main Jun 22, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants